/**
 *  This file is part of OhSnap.
 *
 * 	Copyright (c) 2009 Gabriel Mendonça and Marcelo Machado
 * 
 *  OhSnap is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  OhSnap is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OhSnap.  If not, see <http://www.gnu.org/licenses/>.
 *  
 */
package br.mm.cg.particles;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PVector;
import br.mm.midi.MidiEndListener;
import br.mm.midi.MidiNote;

/**
 * @author machado
 * 
 */
public class GeneratorManager implements MidiEndListener {

	// Default Generator capacity
	private static final int GENERATOR_CAPACITY = 50;
	// Default explosion probability for 10 Generators
	private static final float EXPLOSION_RATE = 7.0E-5f;

	private PApplet parent;
	private List<Generator> generators;
	private PVector max;
	private PVector min;
	private boolean ending;

	public float[] mainColor;

	/**
	 * Constructor.
	 * 
	 * @param pApplet
	 *            the PApplet where this GeneratorManager will work
	 * @param max
	 *            the upper bound for the coordinates
	 * @param min
	 *            the bottom bound for the coordinates
	 * @param color 
	 */
	public GeneratorManager(PApplet pApplet, PVector max, PVector min, float[] color) {
		this.parent = pApplet;
		this.mainColor = color;
		this.max = max;
		this.min = min;

		initGenerators();
	}

	/**
	 * Draws the Generators and update theirs positions.
	 * 
	 * @param notes
	 *            the notes to witch the generators must move
	 * @param pGraphics 
	 */
	public void drawAll(List<MidiNote> notes, PGraphics pGraphics) {
		if (generators.size() > 0) {
			addParticlesByNotes(notes);

			if (generators.size() > 1) {
				Collections.sort(generators);
			}

			List<Generator> newList = new LinkedList<Generator>();

			for (Generator g : generators) {
				g.draw(pGraphics);
				g.move();
				updateByNotes(g, notes);

				doExplosion(g, newList);
				doSplit(g, newList);

				if (!g.isDead) {
					newList.add(g);
				}
			}

			// Updates the generator list with the generators
			// created by split and fusion operations.
			generators = newList;

		} else if (!ending) {
			initGenerators();

		} else {
			parent.exit();
		}
	}

	/**
	 * Initializes the Generators list with a single Particle Generator.
	 */
	private void initGenerators() {
		this.generators = new LinkedList<Generator>();

		PVector position = PVector.add(max, min);
		position.mult(0.5f);
		Generator g = new Generator(this.parent, position, GENERATOR_CAPACITY,
				this.max, this.min, null);

		g.setColor(mainColor);
		generators.add(g);
	}

	/**
	 * For each MidiNote, adds a particle to a random generator.
	 * 
	 * @param notes
	 *            the MidiNote list
	 */
	private void addParticlesByNotes(List<MidiNote> notes) {
		int size = notes.size();
		for (int i = 0; i < size; i++) {
			int random = (int) parent.random(generators.size());
			generators.get(random).addParticle();
		}
	}

	/**
	 * Updates the generator's destination, accordingly to the notes played.
	 * 
	 * @param generator
	 *            the generator to be updated
	 * @param notes
	 *            the notes to witch the generator must move
	 */
	private void updateByNotes(Generator generator, List<MidiNote> notes) {
		// The iteration must be synchronized to avoid concurrency problems.
		synchronized (notes) {
			for (MidiNote note : notes) {
//				PApplet.println(note.getVelocity());
				if (note.getVelocity() >= 100.0f) {
					PVector movement = noteMovement(note);
					float movementNoise = parent.noise(generator.getPosition().x,
							generator.getPosition().y, generator.getPosition().z);
					movement.mult(movementNoise);
	
					PVector finalMovement = PVector.mult(movement, generator
							.getMultiplier());
	
					generator.getDestination().add(finalMovement);
				}
			}
		}
	}

	/**
	 * If the Generator is full, adds its children to the new Generator list.
	 * 
	 * @param generator
	 *            the Generator that will be tested
	 * @param newList
	 *            the new Generator list.
	 */
	private void doSplit(Generator generator, List<Generator> newList) {
		if (generator.isFull()) {
			newList.addAll(splitGenerator(generator));
			generator.isDead = true;
		}
	}

	/**
	 * Splits the Generator, returning a list with its children.
	 * 
	 * @param generator
	 *            the Generator that will be split
	 * @return a list with the generator's children
	 */
	private List<Generator> splitGenerator(Generator generator) {
		List<Generator> list = new LinkedList<Generator>();

		PVector position = generator.getPosition();
		PVector destination = generator.getDestination();
		PVector movement = generator.getRandomMovement();
		movement.mult(generator.getRadius() / 10.0f);
		int color = generator.getP5Color();
		int blendColor;

		List<Particle> particles = generator.getParticles();
		int size = particles.size();

		for (int i = 0; i < 2; i++) {
			List<Particle> subList = particles.subList(i * (size / 2), (i + 1)
					* (size / 2));
			List<Particle> part = new LinkedList<Particle>(subList);

			Generator g = new Generator(parent, position.get(), generator
					.getCapacity(), max, min, part);
			g.setDestination(destination.get());

			g.setColor(new float[] { parent.random(256), parent.random(256),
					parent.random(256) });
			blendColor = PApplet.blendColor(color, g.getP5Color(),
					PApplet.SOFT_LIGHT);
			g.setColor(blendColor);

			for (Particle p : g.getParticles()) {
				p.position = PVector.sub(generator.transformedCenter, position);
			}

			list.add(g);

			movement.mult(-1);
		}

		// Make them move in opposite directions
		list.get(1).getMultiplier().mult(-1.0f);

		return list;
	}

	/**
	 * Does the Generator's explosion, according to the rate of explosion.
	 * 
	 * @param generator
	 *            the Generator that could explode
	 * @param newList
	 *            the new Generator list
	 */
	private void doExplosion(Generator generator, List<Generator> newList) {
		if (this.generators.size() > 1 && !generator.isDead) {
			// Makes the explosion probability proportional to the number of
			// Generators
			float probability = (float) generators.size() * EXPLOSION_RATE
					/ 10.0f;
			int random = (int) parent.random(1.0f / probability);

			// Begin explosion
			if (random == 0) {
				Generator explosion = new Explosion(generator);
				newList.add(explosion);
				generator.isDead = true;
			}

		}
	}

	/**
	 * Determines the movement associated with a single note.
	 * 
	 * @param note
	 *            the note witch will have it's movement determined
	 */
	private PVector noteMovement(MidiNote note) {
		PVector movement;
		float sqrt2 = PApplet.sqrt(2);

		switch (note.getNote()) {
		case C:
			movement = new PVector(1f, 0f, 0f);
			break;
		case Cs:
			movement = new PVector(-1f, 0f, 0f);
			break;
		case D:
			movement = new PVector(0f, 1f, 0f);
			break;
		case Ds:
			movement = new PVector(0f, -1f, 0f);
			break;
		case E:
			movement = new PVector(0f, 0f, 1f);
			break;
		case F:
			movement = new PVector(0f, 0f, -1f);
			break;
		case Fs:
			movement = new PVector(sqrt2, sqrt2, 0f);
			break;
		case G:
			movement = new PVector(-sqrt2, -sqrt2, 0f);
			break;
		case Gs:
			movement = new PVector(0f, sqrt2, sqrt2);
			break;
		case A:
			movement = new PVector(0f, -sqrt2, -sqrt2);
			break;
		case As:
			movement = new PVector(sqrt2, 0f, sqrt2);
			break;
		case B:
			movement = new PVector(-sqrt2, 0f, -sqrt2);
			break;
		default:
			movement = new PVector();
		}
		movement.mult(note.getOctave() * 20.0f);
		return movement;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see br.mm.midi.MidiEndListener#midiEnd()
	 */
	/**
	 * Called when the music ends, explodes all generators.
	 */
	@Override
	public void midiEnd() {
		List<Generator> newList = new LinkedList<Generator>();

		for (Generator g : this.generators) {
			Generator explosion = new Explosion(g);
			newList.add(explosion);
		}

		this.generators = newList;
		this.ending = true;
	}

}
